package com.google.gwt.gwtpages.client.page;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Stack;

import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.gwtpages.client.GWTPagesSettings;
import com.google.gwt.gwtpages.client.event.PageShownEvent;
import com.google.gwt.gwtpages.client.page.loader.GWTPagesLoader;
import com.google.gwt.gwtpages.client.page.loader.PageLoadCallback;
import com.google.gwt.gwtpages.client.page.parameters.PageParameters;
import com.google.gwt.gwtpages.client.page.parameters.SimplePageParameters;
import com.google.gwt.user.client.History;

/**
 * Controller class used to move from 1 page to another within a GWT Pages
 * application. When using this class, the
 * {@link GWTPagesSettings#init(PagePresenter, GWTPagesLoader, com.google.gwt.event.shared.HandlerManager)}
 * method must be called first. There are several different gotoPage methods
 * which can be used depending on the context.
 * 
 * When the application initially loads, you also usually want to call
 * {@link GWTPages#showStartPage(boolean)}
 * 
 * @author Joe Hudson
 */
public class GWTPages implements ValueChangeHandler<String> {

	private static GWTPages instance;

	GWTPagesSettings settings;
	private boolean keepGoing = true;
	private GotoPageCommand lastCommand;

	private Map<String, GWTPage> pageMap = new HashMap<String, GWTPage>();
	private Stack<PageStackItem> pageStack = new Stack<PageStackItem>();

	public static GWTPages get() {
		if (null == instance)
			instance = new GWTPages();
		return instance;
	}

	public GWTPages(GWTPagesSettings settings) {
		if (null == settings)
			throw new IllegalArgumentException("The settings are null");
		this.settings = settings;
		if (null == instance)
			instance = this;
	}

	public GWTPages() {
		settings = GWTPagesSettings.get();
		if (null == instance)
			instance = this;
	}

	/**
	 * Go to the start page.
	 * 
	 * @param useHistoryToken
	 *            true if the history token should be evaluated to obtain the
	 *            page token and false to use the standard default page
	 */
	public void showStartPage(boolean useHistoryToken) {
		String token = GWTPagesLoader.PAGE_DEFAULT;
		if (useHistoryToken)
			token = History.getToken();
		if (null == token)
			token = GWTPagesLoader.PAGE_DEFAULT;
		onNewPage(token, true);
	}

	public Stack<PageStackItem> getPageStack() {
		return pageStack;
	}

	public GotoPageCommand gotoPage(String pageToken) {
		return new GotoPageCommand(pageToken, this);
	}

	public GotoPageCommand gotoPage(String pageToken, PageRequestSession session) {
		return new GotoPageCommand(pageToken, session, this);
	}

	void gotoPage(GotoPageCommand command) {
		keepGoing = true;
		for (PageEventHandler handler : settings.getPageEventHandlers()) {
			handler.onPageRequest(command.getPageToken(), null, command.getSession());
		}
		String pageToken = command.getPageToken();
		GWTPage pageData = pageMap.get(pageToken);
		String historyToken = null;
		PageParameters parameters = null;
		if (null != command.getParameterList(false)) {
			Serializable[] params = command.getParameterList(false).toArray(
					new Serializable[command.getParameterList(false).size()]);
			historyToken = createHistoryToken(pageToken, params);
			parameters = new SimplePageParameters(pageToken, historyToken,
					params, null);
		} else if (null != command.getParameterMap(false)) {
			historyToken = createHistoryToken(pageToken,
					command.getParameterMap(false));
			parameters = new SimplePageParameters(pageToken, historyToken,
					null, command.getParameterMap(false));
		} else {
			historyToken = createHistoryToken(pageToken);
			parameters = new SimplePageParameters(pageToken, historyToken,
					null, null);
		}

		if (command.isAddHistoryToken())
			History.newItem(historyToken, false);

		if (null != pageData) {
			gotoPage(pageData, parameters, command);
		} else {
			settings.getPageLoader().getPage(pageToken,
					new _PageLoadCallback(parameters, command));
		}
	}

	private void onNewPage(String historyToken, boolean addHistoryToken) {
		keepGoing = true;
		for (PageEventHandler handler : settings.getPageEventHandlers()) {
			handler.onPageRequest(null, historyToken, null);
		}
		if (!keepGoing()) return;
		String pageToken = null;
		if (null == historyToken
				|| historyToken.equals(GWTPagesLoader.PAGE_DEFAULT)) {
			pageToken = GWTPagesLoader.PAGE_DEFAULT;
		} else {
			Iterator<String> possibleTokens = settings.getPageTokenizer()
					.getPossiblePageTokens(historyToken);
			while (possibleTokens.hasNext()) {
				String token = possibleTokens.next();
				if (settings.getPageLoader().isValidPageToken(token)) {
					pageToken = token;
				}
			}
		}
		if (null == pageToken) {
			onPageNotFound(null, historyToken);
			return;
		}

		PageParameters parameters = settings.getPageTokenizer()
				.getPageParameters(historyToken, pageToken);
		GWTPage pageLoadResult = pageMap.get(pageToken);
		if (null != pageLoadResult) {
			// the page was cached
			gotoPage(pageLoadResult, parameters, new GotoPageCommand(pageToken,
					this).addHistoryToken(addHistoryToken));
		} else {
			// the page couldn't be found
			settings.getPageLoader().getPage(
					pageToken,
					new _PageLoadCallback(parameters, new GotoPageCommand(
							pageToken, this).addHistoryToken(addHistoryToken)));
		}
	}

	private boolean keepGoing() {
		if (keepGoing) return true;
		else return false;
	}

	/** Async page loading callbacks **/
	private void onPageFound(GWTPage pageLoadResult,
			PageParameters pageParameters, GotoPageCommand command) {
		for (PageEventHandler handler : settings.getPageEventHandlers()) {
			handler.onPageLoaded(pageLoadResult);
		}
		if (!keepGoing()) return;
		pageMap.put(pageLoadResult.getPageToken(), pageLoadResult);
		gotoPage(pageLoadResult, pageParameters, command);
	}

	private void onPageNotFound(String pageToken, String historyToken) {
		for (PageEventHandler handler : settings.getPageEventHandlers()) {
			handler.onPageNotFound(historyToken);
		}
	}

	private void onPageLoadFailure(String pageToken, String historyToken,
			Throwable cause) {
		for (PageEventHandler handler : settings.getPageEventHandlers()) {
			handler.onPageLoadFailure(historyToken, cause);
		}
	}

	private void gotoPage(GWTPage page, PageParameters parameters,
			GotoPageCommand command) {
		command.getSession().setPageAttributes(page.getAttributes());
		page = page.copy();
		lastCommand = command;
		for (PageEventHandler handler : settings.getPageEventHandlers()) {
			handler.beforePageShow(page, parameters, command.getSession());
		}
		if (!keepGoing()) return;
		GWTPage previousPage = null;
		if (pageStack.size() > 0) {
			previousPage = pageStack.peek().getPageData();
			if (!pageStack.peek().shouldKeepInStack())
				pageStack.pop();
		}
		// FIXME need a way for pages to determine if they are kept in the page stack
		pageStack.push(new PageStackItem(command, page, parameters, false));

		try {
			page.getPresenter().onShowPage(
					parameters,
					command.getSession(),
					new _AsyncPageCallback(page, previousPage, parameters,
							command));
			for (PageEventHandler handler : settings.getPageEventHandlers()) {
				handler.afterPageShow(page, parameters, command.getSession());
			}
		} catch (Throwable t) {
			for (PageEventHandler handler : settings.getPageEventHandlers()) {
				handler.onPageShowFailure(page, parameters,
						command.getSession());
			}
		}
	}

	/**
	 * History token change listener
	 */
	public void onValueChange(ValueChangeEvent<String> event) {
		onNewPage(event.getValue(), true);
	}

	/**
	 * Create and return the history token represented by the page token and
	 * additional parameters
	 * 
	 * @param pageToken
	 *            the page token
	 * @param parameters
	 *            input parameters
	 */
	public String createHistoryToken(String pageToken,
			Serializable... parameters) {
		return settings.getPageTokenizer().createHistoryToken(pageToken,
				parameters);
	}

	/**
	 * Create and return the history token represented by the page token and
	 * additional parameters
	 * 
	 * @param pageToken
	 *            the page token
	 * @param parameters
	 *            input parameters
	 */
	public String createHistoryToken(String pageToken,
			Map<String, Serializable> parameters) {
		return settings.getPageTokenizer().createHistoryToken(pageToken,
				parameters);
	}

	/**
	 * Stop the current request processing
	 */
	public void stopRequest() {
		keepGoing = false;
	}

	public GotoPageCommand getLastCommand() {
		return lastCommand;
	}

	/** Async callback classes */
	private class _AsyncPageCallback implements AsyncPageCallback {
		private GWTPage previousPage;
		private GWTPage page;
		private PageParameters pageParameters;
		private GotoPageCommand command;

		private _AsyncPageCallback(GWTPage page, GWTPage previousPage,
				PageParameters pageParameters, GotoPageCommand command) {
			this.page = page;
			this.previousPage = previousPage;
			this.pageParameters = pageParameters;
			this.command = command;
		}

		public void onSuccess() {
			settings.getApplicationPresenter().showPage(page, pageParameters, command.getSession());
			for (PageEventHandler handler : settings.getPageEventHandlers()) {
				handler.onPageShowSuccess(page, pageParameters,
						command.getSession());
			}
			settings.getEventBus().fireEvent(
					new PageShownEvent(page, previousPage, pageStack));
		}

		public void onFailure(Throwable cause) {
			for (PageEventHandler handler : settings.getPageEventHandlers()) {
				handler.onPageShowFailure(page, pageParameters,
						command.getSession());
			}
		}
	}

	private class _PageLoadCallback implements PageLoadCallback {
		private PageParameters parameters;
		private GotoPageCommand command;

		public _PageLoadCallback(PageParameters parameters,
				GotoPageCommand command) {
			this.parameters = parameters;
			this.command = command;
		}

		public void onPageFound(GWTPage pageLoadResult) {
			GWTPages.this.onPageFound(pageLoadResult, parameters, command);
		}

		public void onPageNotFound(String pageToken) {
			GWTPages.this.onPageNotFound(pageToken,
					parameters.getHistoryToken());
		}

		public void onPageLoadFailure(String pageToken, Throwable cause) {
			GWTPages.this.onPageLoadFailure(pageToken,
					parameters.getHistoryToken(), cause);
		}
	}
}